<?php

namespace chick1993\util\sheet;

use chick1993\util\libs\base\SheetBase;
use chick1993\util\libs\exceptions\CellException;
use chick1993\util\libs\exceptions\CellsException;
use chick1993\util\libs\exceptions\RuntimeException;
use Vtiful\Kernel\Excel;

/**
 * 所有涉及行号均从0开始，0表示第一行，1表示第二行，2表示第三行。
 * 示例，
 * ```
 * $keys = [ "id", "name", "pay_no", "order_no", ];
 * $filename = "import_path/file.xlsx";
 * ```
 * 1。读取数据
 * ```
 *  $read = Read::file($filename)->relation($keys);
 *  $data = $read->getArray(1);
 * ```
 * 2.读取、验证并导出错误数据
 * ```
 *  $read = Read::file($filename)->relation($keys)->format([
 *      'pay_no'   => Excel::TYPE_STRING,
 *      'order_no' => Excel::TYPE_STRING,
 *      1 => Excel::TYPE_STRING,
 *  ]);
 *  // 获取第一行
 *  $header = $read->getRow(0);
 *  // 获取第二行到最后，并添加验证
 *  $data = $read->valid(function ($row, $num) {
 *      // 直接抛出单个异常
 *      // if (empty($row['xxx'])) {
 *      //    throw new ColException(1, '不能为空');
 *      // }
 *      // 抛出多个异常
 *      $exception = [];
 *      if (empty($row['xxx1'])) {
 *          $exception[] = new CellException(1, '不能为空');
 *      }
 *      if (empty($row['xxx2'])) {
 *          $exception[] = new CellException(2, '不能为空');
 *       }
 *      throw new CellsException($exception);
 *  })->getArray(1);
 *  if ($read->hasRowException()) {
 *      Write::file(runtime_path() . 'export/test.xlsx')->errors($read)->output();
 *  }
 * ```
 * @method \Generator nextRow(int $start = 0, int $length = 0, bool $valid = true)
 * @method self openSheet(string $sheetName = null, int $skipFlag = 0x01)
 */
class Read extends SheetBase
{
    const TYPE_STRING      = Excel::TYPE_STRING;
    const TYPE_INT         = Excel::TYPE_INT;
    const TYPE_DOUBLE      = Excel::TYPE_DOUBLE;
    const TYPE_TIMESTAMP   = Excel::TYPE_TIMESTAMP;

    /**
     * @var string 要读取的工作表
     */
    protected $sheetName = null;

    /**
     * @var int 跳过规则，详见XlsWrite
     */
    protected $skipFlag = 0x01;

    /**
     * @var array 读取为关联数组时的键名
     */
    protected $field = [];

    /**
     * @var callable 校验回调
     */
    protected $validCallback;
    /**
     * @var callable 数据处理回调
     */
    protected $rowCallback;

    protected $isCellCallback = false;

    protected function __construct(string $filename)
    {
        $this->_formatFileInfo($filename);

        $excel = new Excel(['path' => $this->path]);
        $this->excel = $excel->openFile($this->name);
    }

    /**
     * 设置文件地址
     * @param string $filename 文件绝对路径，支持 xlsx 和 csv
     * @return self
     */
    static public function file(string $filename): self
    {
        return new self($filename);
    }

    /**
     * 指定打开的工作表
     * @param string|null $sheetName
     * @param int $skipFlag
     * @return self
     */
    protected function _openSheetCall(string $sheetName = null, int $skipFlag = 0x01): self
    {
        $this->sheetName = $sheetName;
        $this->skipFlag = $skipFlag;
        return $this;
    }

    /**
     * 设置字段关联，将作为关联数组的键
     * @param array $field [0=>filed0,1=>field1,3=>field3,...]
     * @param bool $useHeader 是否用表格的表头作为其余字段的索引
     * @param int $headerRow 表头行，默认0
     * @return $this
     */
    public function relation(array $field, bool $useHeader = false, int $headerRow = 0): self
    {
        $headerRow = max($headerRow, 0);
        $header = $this->getRow($headerRow);
        if (!$useHeader) $header = array_keys($header);
        $field += $header;
        ksort($field);
        array_splice($field, count($header));
        $this->field = $field;
        $this->format = [];// 重新设置关联字段后，格式需重新关联
        return $this;
    }

    /**
     * 验证行数据
     * @param callable $call 验证回调，抛出RowException会自动记录至异常 function(array $rowData, int $rowNum)
     * @return self
     */
    public function valid(callable $call): self
    {
        if (is_callable($call)) {
            $this->validCallback = $call;
        }
        return $this;
    }

    /**
     * 行数据
     * @param callable $call 异常会被抛出
     *     按照行回调 function(array $rowData, int $rowNum)
     *     按单元格回调 function(mixed $cellVal, int $rowNum, $colKey)
     * @param bool $cell 是否按单元格回调
     * @return self
     */
    public function callback(callable $call, bool $cell = false): self
    {
        if (is_callable($call)) {
            $this->isCellCallback = $cell;
            $this->rowCallback = $call;
        }
        return $this;
    }

    /**
     * 格式化回调，返回统一回调入口
     * @param bool $valid
     * @return callable
     */
    private function _getCallBack(bool $valid = true): callable
    {
        $defaultCall = function (array $rowData, int $rowNum) {
            return $rowData;
        };

        $validCallback = is_callable($this->validCallback) && $valid ? $this->validCallback : $defaultCall;
        if (is_callable($this->rowCallback)) {
            if ($this->isCellCallback) {
                $rowCallback = function (array $rowData, int $rowNum) {
                    foreach ($rowData as $key => $val) {
                        $rowData[$key] = ($this->rowCallback)($val, $rowNum, $key);
                    }
                    return $rowData;
                };
            } else {
                $rowCallback = $this->rowCallback;
            }
        } else {
            $rowCallback = $defaultCall;
        }

        return function (array $rowData, int $rowNum) use ($validCallback, $rowCallback) {
            $validRow = $rowData;// 避免数据被验证回调修改
            $validCallback($validRow, $rowNum);
            unset($validRow);

            try {
                $rowData = $rowCallback($rowData, $rowNum);
            } catch (\Throwable $e) {
                throw new RuntimeException($e);
            }
            return $rowData;
        };
    }

    /**
     * 是否有数据验证异常
     * @return bool
     */
    public function hasRowException(): bool
    {
        return !empty($this->rowExceptions);
    }

    /**
     * getIterator别名
     * @param int $start 开始读取行数 0-第1行
     * @param int $length 读取总行数 0-全部
     * @param bool $valid 是否触发验证
     * @return \Generator
     * @see getIterator
     */
    protected function _nextRowCall(int $start = 0, int $length = 0, bool $valid = true): \Generator
    {
        return $this->getIterator($start, $length, $valid);
    }

    /**
     * 读取数据(数组,全量读取)
     * @param int $start 开始读取行数 0-第1行
     * @param int $length 读取总行数 0-全部
     * @param bool $valid 是否触发验证
     * @return array
     */
    public function getArray(int $start = 0, int $length = 0, bool $valid = true): array
    {
        $data = [];
        $itr = $this->getIterator($start, $length, $valid);
        foreach ($itr as $k => $item) {
            $data[$start + $k] = $item;
        }
        return $data;
    }

    /**
     * 返回使用 callback 处理后的数据，callback 返回 null ,则返回原数据
     * @param callable $call
     * @param int $start
     * @param int $length
     * @param bool $valid
     * @return array
     */
    public function getArrayCallback(callable $call, int $start = 0, int $length = 0, bool $valid = true): array
    {
        $data = $this->getArray($start, $length, $valid);
        $res = [];
        try {
            $res = call_user_func_array($call, [$data]);
        } catch (CellsException $e) {
            $err = $e->getError();
            foreach ($err as $item) {
                $this->rowExceptions[$item['row']][] = $item;
            }
        }
        return is_array($res) ? $res : $data;
    }

    public function getFormat(): array
    {
        if (empty($this->field)) return $this->originalFormat;
        if (empty($this->format)) {
            $fields = array_flip($this->field);
            $formats = [];
            foreach ($this->originalFormat as $key => $format) {
                if (!is_numeric($key)) {
                    $key = $fields[$key] ?? null;
                }
                if (isset($key)) {
                    $formats[$key] = $format;
                }
            }
            $this->format = $formats;
        }
        return $this->format;
    }

    /**
     * 读取数据(迭代器)
     * @param int $start 开始读取行数 0-第1行
     * @param int $length 读取总行数 0-全部
     * @param bool $valid 是否触发验证
     * @return \Generator
     */
    public function getIterator(int $start = 0, int $length = 0, bool $valid = true): \Generator
    {
        $checkLength = $length > 0;
        $relation = !empty($this->field);
        $totalField = count($this->field);
        $callback = $this->_getCallBack($valid);

        $itr = $this->_getRowIterator($start, $length);
        foreach ($itr as $index => $row) {
            try {
                if ($checkLength && $index >= $length) break;
                if ($relation) {
                    if (count($row) > $totalField) {// 裁切表头部分。避免报错
                        array_splice($row, $totalField);
                    } else if (count($row) < $totalField) {// 补齐表头部分。避免报错
                        $row = array_pad($row, $totalField, null);
                    }
                    // 关联数组索引
                    $row = array_combine($this->field, $row);
                }
                $row = $callback($row, $index);
            } catch (CellException $e) {
                $rowIndex = $index + $start;
                $e->setRowIndex($rowIndex);
                $this->rowExceptions[$rowIndex][] = $e->getError();
            } catch (CellsException $e) {
                $rowIndex = $index + $start;
                $e->setRowIndex($rowIndex);
                $this->rowExceptions[$rowIndex] = $e->getError();
            }

            yield $row;
        }
    }

    /**
     * 读取数据(指定行)
     * @param int $row 读取行，0-第一行
     * @param bool $valid 是否触发验证
     * @return mixed|null
     */
    public function getRow(int $row, bool $valid = false)
    {
        $data = $this->getArray($row, 1, $valid);
        $data = array_values($data);
        return $data[0] ?? null;
    }

    /**
     * 跳过指定行数
     * @param int $skipRows 0-第一行
     * @return void
     * @deprecated 不支持该操作，起始行直接在获取数据方法传入
     */
    public function setSkipRows(int $skipRows)
    {
        throw new RuntimeException('不支持该操作，起始行直接在获取数据方法传入');
    }
}